Popular Searches
Popular Course Categories
Popular Courses

Validating email, password, and other inputs

Validating email, password, and other inputs

Flutter Forms & User Input

Validating Email, Password, and Other Inputs in Flutter – Detailed Notes

Input validation is an important part of Flutter application development. It checks whether information entered by a user follows the required rules before the application processes, stores, or submits that information.

Flutter provides Form, TextFormField, FormField, validator, FormState, and AutovalidateMode to build validation workflows. A validator returns an error message when input is invalid and null when the input is valid. The form can then be checked using FormState.validate(). Flutter Official Form Validation Guide


1. What Is Input Validation?

Input validation is the process of checking user-entered data against predefined rules.

For example:

  • Name should not be empty.
  • Email should have an acceptable email format.
  • Password should meet the application's security requirements.
  • Confirm password should match the password.
  • Phone number should contain the required number of digits.
  • Age should be within an acceptable range.
  • A required dropdown should have a selected value.
  • A required checkbox should be selected.
  • Address should not be empty.
  • Username should contain only allowed characters.

2. Why Validate User Input?

  • Prevents incomplete data submission.
  • Provides useful feedback to users.
  • Reduces invalid data sent to a backend.
  • Improves application reliability.
  • Improves the overall user experience.
  • Helps users correct mistakes before submission.
  • Ensures required fields contain appropriate values.

Flutter's official form validation workflow uses a Form, a GlobalKey, field validators, and validate() to determine whether form input is valid. Read the official Flutter validation recipe


3. Main Widgets Used for Input Validation

Widget/Class Purpose
Form Groups multiple form fields.
TextFormField Creates a text field that integrates with Form validation.
FormField Provides the base functionality for validated form fields.
GlobalKey Provides access to the form state.
FormState.validate() Runs validation for the form fields.
validator Contains the validation rules for a field.
AutovalidateMode Controls automatic validation behavior.
TextEditingController Reads and controls text input.
inputFormatters Can restrict or transform input before validation.

TextFormField wraps a text field and integrates it with the enclosing Form, providing validation and other FormField functionality. Flutter TextFormField Documentation


4. Basic Validation Structure

The basic validation structure contains a Form, one or more fields, validators, and a submit button.

final _formKey = GlobalKey();

Form(
  key: _formKey,
  child: Column(
    children: [
      TextFormField(
        validator: (value) {
          if (value == null || value.trim().isEmpty) {
            return 'This field is required';
          }
          return null;
        },
      ),
      ElevatedButton(
        onPressed: () {
          if (_formKey.currentState!.validate()) {
            print('Form is valid');
          }
        },
        child: const Text('Submit'),
      ),
    ],
  ),
)

5. Understanding the validator Function

The validator function receives the current field value and checks it against validation rules.

validator: (value) {
  if (value == null || value.trim().isEmpty) {
    return 'This field is required';
  }
  return null;
}
Validator Result Meaning
Error message The input is invalid and the error is displayed.
null The input is considered valid.

The Flutter API specifies that the validator returns an error string for invalid input or null when there is no validation error. FormField validator API


6. Required Field Validation

Required-field validation checks whether the user has entered a value.

TextFormField(
  decoration: const InputDecoration(
    labelText: 'Full Name',
    border: OutlineInputBorder(),
  ),
  validator: (value) {
    if (value == null || value.trim().isEmpty) {
      return 'Please enter your full name';
    }
    return null;
  },
)

Using trim()

final name = value?.trim() ?? '';

if (name.isEmpty) {
  return 'Name is required';
}

Using trim() is useful when spaces should not be treated as meaningful input at the beginning or end of a field.


7. Email Validation

Email validation checks whether the entered value follows an acceptable email pattern.

Simple Email Validation

TextFormField(
  keyboardType: TextInputType.emailAddress,
  decoration: const InputDecoration(
    labelText: 'Email Address',
    prefixIcon: Icon(Icons.email),
    border: OutlineInputBorder(),
  ),
  validator: (value) {
    if (value == null || value.trim().isEmpty) {
      return 'Email is required';
    }

    if (!value.contains('@')) {
      return 'Enter a valid email address';
    }

    return null;
  },
)

Regular Expression Email Validation

bool isValidEmail(String email) {
  return RegExp(
    r'^[^@\s]+@[^@\s]+\.[^@\s]+$',
  ).hasMatch(email);
}

Use the function inside a validator:

validator: (value) {
  final email = value?.trim() ?? '';

  if (email.isEmpty) {
    return 'Email is required';
  }

  if (!isValidEmail(email)) {
    return 'Enter a valid email address';
  }

  return null;
}

Email Examples

Input Expected Result
[email protected] Valid
[email protected] Valid
user@ Invalid
user.com Invalid
@example.com Invalid
Empty value Invalid

8. Password Validation

Password validation checks whether the password satisfies the application's required rules.

Minimum Password Length

TextFormField(
  obscureText: true,
  decoration: const InputDecoration(
    labelText: 'Password',
    prefixIcon: Icon(Icons.lock),
    border: OutlineInputBorder(),
  ),
  validator: (value) {
    if (value == null || value.isEmpty) {
      return 'Password is required';
    }

    if (value.length < 8) {
      return 'Password must contain at least 8 characters';
    }

    return null;
  },
)

Password with Multiple Rules

validator: (value) {
  final password = value ?? '';

  if (password.isEmpty) {
    return 'Password is required';
  }

  if (password.length < 8) {
    return 'Minimum 8 characters required';
  }

  if (!RegExp(r'[A-Z]').hasMatch(password)) {
    return 'Include at least one uppercase letter';
  }

  if (!RegExp(r'[a-z]').hasMatch(password)) {
    return 'Include at least one lowercase letter';
  }

  if (!RegExp(r'[0-9]').hasMatch(password)) {
    return 'Include at least one number';
  }

  return null;
}

Password Validation Rules

Rule Example
Required Password cannot be empty.
Minimum length At least 8 characters.
Uppercase At least one A-Z character.
Lowercase At least one a-z character.
Number At least one digit.
Special character At least one symbol when required by the application's policy.

9. Confirm Password Validation

Confirm password validation ensures that the password entered in the confirmation field matches the original password.

final passwordController = TextEditingController();

TextFormField(
  controller: passwordController,
  obscureText: true,
  decoration: const InputDecoration(
    labelText: 'Password',
    border: OutlineInputBorder(),
  ),
),

const SizedBox(height: 16),

TextFormField(
  obscureText: true,
  decoration: const InputDecoration(
    labelText: 'Confirm Password',
    border: OutlineInputBorder(),
  ),
  validator: (value) {
    if (value == null || value.isEmpty) {
      return 'Please confirm your password';
    }

    if (value != passwordController.text) {
      return 'Passwords do not match';
    }

    return null;
  },
)

10. Username Validation

A username validator can restrict the minimum length and allowed characters.

TextFormField(
  decoration: const InputDecoration(
    labelText: 'Username',
    prefixIcon: Icon(Icons.person),
    border: OutlineInputBorder(),
  ),
  validator: (value) {
    final username = value?.trim() ?? '';

    if (username.isEmpty) {
      return 'Username is required';
    }

    if (username.length < 4) {
      return 'Username must contain at least 4 characters';
    }

    if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(username)) {
      return 'Use only letters, numbers, and underscores';
    }

    return null;
  },
)

11. Name Validation

TextFormField(
  decoration: const InputDecoration(
    labelText: 'Full Name',
    border: OutlineInputBorder(),
  ),
  validator: (value) {
    final name = value?.trim() ?? '';

    if (name.isEmpty) {
      return 'Name is required';
    }

    if (name.length < 2) {
      return 'Name must contain at least 2 characters';
    }

    return null;
  },
)

12. Phone Number Validation

Phone validation depends on the application's supported regions and expected format. For a simple 10-digit example:

TextFormField(
  keyboardType: TextInputType.phone,
  decoration: const InputDecoration(
    labelText: 'Phone Number',
    prefixIcon: Icon(Icons.phone),
    border: OutlineInputBorder(),
  ),
  validator: (value) {
    final phone = value?.trim() ?? '';

    if (phone.isEmpty) {
      return 'Phone number is required';
    }

    if (!RegExp(r'^\d{10}$').hasMatch(phone)) {
      return 'Enter a valid 10-digit phone number';
    }

    return null;
  },
)

Example Inputs

Input Result
9876543210 Valid for this example
12345 Invalid
98765abc10 Invalid
Empty Invalid

13. Age Validation

Age validation can use int.tryParse() to safely convert text to an integer.

TextFormField(
  keyboardType: TextInputType.number,
  decoration: const InputDecoration(
    labelText: 'Age',
    border: OutlineInputBorder(),
  ),
  validator: (value) {
    final text = value?.trim() ?? '';

    if (text.isEmpty) {
      return 'Age is required';
    }

    final age = int.tryParse(text);

    if (age == null) {
      return 'Enter a valid number';
    }

    if (age < 18) {
      return 'Age must be 18 or above';
    }

    if (age > 120) {
      return 'Enter a valid age';
    }

    return null;
  },
)

14. Number Validation

TextFormField(
  keyboardType: TextInputType.number,
  decoration: const InputDecoration(
    labelText: 'Quantity',
    border: OutlineInputBorder(),
  ),
  validator: (value) {
    final quantity = int.tryParse(value?.trim() ?? '');

    if (quantity == null) {
      return 'Enter a valid number';
    }

    if (quantity <= 0) {
      return 'Quantity must be greater than zero';
    }

    return null;
  },
)

15. Minimum and Maximum Length Validation

Minimum Length

validator: (value) {
  if (value == null || value.isEmpty) {
    return 'This field is required';
  }

  if (value.length < 5) {
    return 'Minimum 5 characters required';
  }

  return null;
}

Maximum Length

validator: (value) {
  if (value == null || value.isEmpty) {
    return 'This field is required';
  }

  if (value.length > 50) {
    return 'Maximum 50 characters allowed';
  }

  return null;
}

Both Minimum and Maximum

validator: (value) {
  final text = value?.trim() ?? '';

  if (text.isEmpty) {
    return 'This field is required';
  }

  if (text.length < 5 || text.length > 50) {
    return 'Enter between 5 and 50 characters';
  }

  return null;
}

16. URL Validation

URL validation can be useful for website, portfolio, social media, or resource fields.

TextFormField(
  keyboardType: TextInputType.url,
  decoration: const InputDecoration(
    labelText: 'Website',
    border: OutlineInputBorder(),
  ),
  validator: (value) {
    final url = value?.trim() ?? '';

    if (url.isEmpty) {
      return 'Website URL is required';
    }

    final uri = Uri.tryParse(url);

    if (uri == null ||
        !uri.hasScheme ||
        !uri.hasAuthority) {
      return 'Enter a valid URL';
    }

    return null;
  },
)

17. Address Validation

TextFormField(
  maxLines: 4,
  decoration: const InputDecoration(
    labelText: 'Address',
    alignLabelWithHint: true,
    border: OutlineInputBorder(),
  ),
  validator: (value) {
    final address = value?.trim() ?? '';

    if (address.isEmpty) {
      return 'Address is required';
    }

    if (address.length < 10) {
      return 'Please enter a complete address';
    }

    return null;
  },
)

18. Pincode Validation

For an Indian 6-digit pincode example:

TextFormField(
  keyboardType: TextInputType.number,
  decoration: const InputDecoration(
    labelText: 'Pincode',
    border: OutlineInputBorder(),
  ),
  validator: (value) {
    final pincode = value?.trim() ?? '';

    if (pincode.isEmpty) {
      return 'Pincode is required';
    }

    if (!RegExp(r'^[1-9][0-9]{5}$').hasMatch(pincode)) {
      return 'Enter a valid 6-digit pincode';
    }

    return null;
  },
)

19. Dropdown Validation

Selection fields can also participate in form validation.

String? selectedRole;

DropdownButtonFormField(
  value: selectedRole,
  decoration: const InputDecoration(
    labelText: 'Select Role',
    border: OutlineInputBorder(),
  ),
  items: const [
    DropdownMenuItem(
      value: 'student',
      child: Text('Student'),
    ),
    DropdownMenuItem(
      value: 'developer',
      child: Text('Developer'),
    ),
    DropdownMenuItem(
      value: 'designer',
      child: Text('Designer'),
    ),
  ],
  onChanged: (value) {
    setState(() {
      selectedRole = value;
    });
  },
  validator: (value) {
    if (value == null) {
      return 'Please select a role';
    }

    return null;
  },
)

Flutter's FormField system is also used by form-integrated selection widgets. Flutter FormField API


20. Checkbox Validation

Checkboxes are often used for required agreements such as terms and conditions.

bool acceptedTerms = false;

CheckboxListTile(
  title: const Text(
    'I accept the Terms and Conditions',
  ),
  value: acceptedTerms,
  onChanged: (value) {
    setState(() {
      acceptedTerms = value ?? false;
    });
  },
)

Before submitting:

if (!acceptedTerms) {
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(
      content: Text(
        'Please accept the Terms and Conditions',
      ),
    ),
  );
  return;
}

21. Radio Button Validation

Radio buttons can be validated by checking whether an option has been selected.

String? selectedGender;

RadioListTile(
  title: const Text('Male'),
  value: 'male',
  groupValue: selectedGender,
  onChanged: (value) {
    setState(() {
      selectedGender = value;
    });
  },
)

Before submission:

if (selectedGender == null) {
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(
      content: Text('Please select an option'),
    ),
  );
  return;
}

22. Date Validation

Date validation can be used for date of birth, appointment dates, registration dates, and other date-based inputs.

DateTime? selectedDate;

Future selectDate() async {
  final pickedDate = await showDatePicker(
    context: context,
    initialDate: DateTime.now(),
    firstDate: DateTime(1950),
    lastDate: DateTime.now(),
  );

  if (pickedDate != null) {
    setState(() {
      selectedDate = pickedDate;
    });
  }
}

Validate the selected date before submission:

if (selectedDate == null) {
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(
      content: Text('Please select a date'),
    ),
  );
  return;
}

23. TextEditingController for Validation

TextEditingController can be used when validation depends on the value of another field or when the application needs direct access to entered text.

final emailController = TextEditingController();

TextFormField(
  controller: emailController,
  keyboardType: TextInputType.emailAddress,
  decoration: const InputDecoration(
    labelText: 'Email',
  ),
)

Read the value:

final email = emailController.text.trim();

Controllers created manually should be disposed when they are no longer needed. Flutter TextEditingController Guide

@override
void dispose() {
  emailController.dispose();
  super.dispose();
}

24. Cross-Field Validation

Cross-field validation means validating one field using the value of another field.

A common example is password confirmation.

final passwordController = TextEditingController();
final confirmPasswordController = TextEditingController();

TextFormField(
  controller: passwordController,
  obscureText: true,
  validator: (value) {
    if (value == null || value.length < 8) {
      return 'Password must contain at least 8 characters';
    }
    return null;
  },
),

TextFormField(
  controller: confirmPasswordController,
  obscureText: true,
  validator: (value) {
    if (value != passwordController.text) {
      return 'Passwords do not match';
    }
    return null;
  },
)

25. Real-Time Validation with onChanged

The onChanged callback can be used to react whenever the user changes the field value.

TextFormField(
  onChanged: (value) {
    print('Current value: $value');
  },
)

This can be useful for password strength indicators, character counters, search suggestions, and other interactive experiences. Flutter's documentation provides both onChanged and TextEditingController approaches for handling text changes. Flutter Text Field Changes Guide


26. Password Strength Indicator

A password strength indicator can provide visual feedback while the user types.

String password = '';

TextFormField(
  obscureText: true,
  onChanged: (value) {
    setState(() {
      password = value;
    });
  },
  decoration: const InputDecoration(
    labelText: 'Password',
    border: OutlineInputBorder(),
  ),
)

Text(
  password.length >= 8
      ? 'Password length requirement met'
      : 'Use at least 8 characters',
)

For a more advanced implementation, the application can separately check length, uppercase characters, lowercase characters, numbers, and special characters.


27. Input Formatting

Input formatters can restrict or transform user input before it reaches the validation logic.

import 'package:flutter/services.dart';

TextFormField(
  keyboardType: TextInputType.phone,
  inputFormatters: [
    FilteringTextInputFormatter.digitsOnly,
  ],
  validator: (value) {
    if (value == null || value.length != 10) {
      return 'Enter a 10-digit phone number';
    }
    return null;
  },
)

For example, FilteringTextInputFormatter.digitsOnly can restrict a field to digits while the validator checks whether the resulting value meets the required length.


28. AutovalidateMode

AutovalidateMode controls when form fields automatically perform validation.

Disabled

TextFormField(
  autovalidateMode: AutovalidateMode.disabled,
  validator: (value) {
    if (value == null || value.isEmpty) {
      return 'Required';
    }
    return null;
  },
)

Automatic validation is disabled.

On User Interaction

TextFormField(
  autovalidateMode: AutovalidateMode.onUserInteraction,
  validator: (value) {
    if (value == null || value.isEmpty) {
      return 'Required';
    }
    return null;
  },
)

The field automatically validates after user interaction.

Always

TextFormField(
  autovalidateMode: AutovalidateMode.always,
  validator: (value) {
    if (value == null || value.isEmpty) {
      return 'Required';
    }
    return null;
  },
)

The field is automatically validated even without user interaction.

On Unfocus

TextFormField(
  autovalidateMode: AutovalidateMode.onUnfocus,
  validator: (value) {
    if (value == null || value.isEmpty) {
      return 'Required';
    }
    return null;
  },
)

The field validates when it loses focus.

On User Interaction If Error

TextFormField(
  autovalidateMode:
      AutovalidateMode.onUserInteractionIfError,
  validator: (value) {
    if (value == null || value.isEmpty) {
      return 'Required';
    }
    return null;
  },
)

Current Flutter provides these validation modes through the AutovalidateMode enum. AutovalidateMode API


29. FormState.validate()

The validate() method runs the validators for the form's descendant fields.

if (_formKey.currentState!.validate()) {
  print('All inputs are valid');
} else {
  print('Some inputs are invalid');
}

When validation finds errors, Flutter rebuilds the form to display the corresponding error messages. Flutter Form Validation


30. FormState.save()

After successful validation, save() can be used to execute the onSaved callback for form fields.

String email = '';

TextFormField(
  decoration: const InputDecoration(
    labelText: 'Email',
  ),
  validator: (value) {
    if (value == null || value.isEmpty) {
      return 'Email is required';
    }
    return null;
  },
  onSaved: (value) {
    email = value ?? '';
  },
)

Then:

if (_formKey.currentState!.validate()) {
  _formKey.currentState!.save();
  print(email);
}

31. Form Reset

The reset() method resets form fields to their initial values and resets their validation state.

OutlinedButton(
  onPressed: () {
    _formKey.currentState!.reset();
  },
  child: const Text('Reset'),
)

32. Focus Management After Validation

Focus can be moved to a particular field when needed, such as after detecting a validation problem.

final emailFocusNode = FocusNode();

TextFormField(
  focusNode: emailFocusNode,
  decoration: const InputDecoration(
    labelText: 'Email',
  ),
)

Request focus:

FocusScope.of(context).requestFocus(emailFocusNode);

Manually created focus nodes should be disposed when no longer needed. Flutter Focus and Text Fields


33. Reusable Email Validator

Reusable validators help prevent duplicated validation code across multiple screens.

String? validateEmail(String? value) {
  final email = value?.trim() ?? '';

  if (email.isEmpty) {
    return 'Email is required';
  }

  if (!RegExp(
    r'^[^@\s]+@[^@\s]+\.[^@\s]+$',
  ).hasMatch(email)) {
    return 'Enter a valid email address';
  }

  return null;
}

Use it like this:

TextFormField(
  validator: validateEmail,
)

34. Reusable Password Validator

String? validatePassword(String? value) {
  final password = value ?? '';

  if (password.isEmpty) {
    return 'Password is required';
  }

  if (password.length < 8) {
    return 'Password must contain at least 8 characters';
  }

  if (!RegExp(r'[A-Z]').hasMatch(password)) {
    return 'Include at least one uppercase letter';
  }

  if (!RegExp(r'[a-z]').hasMatch(password)) {
    return 'Include at least one lowercase letter';
  }

  if (!RegExp(r'[0-9]').hasMatch(password)) {
    return 'Include at least one number';
  }

  return null;
}

Use it like this:

TextFormField(
  obscureText: true,
  validator: validatePassword,
)

35. Reusable Phone Validator

String? validatePhone(String? value) {
  final phone = value?.trim() ?? '';

  if (phone.isEmpty) {
    return 'Phone number is required';
  }

  if (!RegExp(r'^\d{10}$').hasMatch(phone)) {
    return 'Enter a valid 10-digit phone number';
  }

  return null;
}

36. Reusable Required Field Validator

String? validateRequired(
  String? value,
  String fieldName,
) {
  if (value == null || value.trim().isEmpty) {
    return '$fieldName is required';
  }

  return null;
}

Usage:

TextFormField(
  validator: (value) {
    return validateRequired(value, 'Full Name');
  },
)

37. Creating a Validator Utility Class

class Validators {
  static String? required(
    String? value,
    String fieldName,
  ) {
    if (value == null || value.trim().isEmpty) {
      return '$fieldName is required';
    }

    return null;
  }

  static String? email(String? value) {
    final email = value?.trim() ?? '';

    if (email.isEmpty) {
      return 'Email is required';
    }

    if (!RegExp(
      r'^[^@\s]+@[^@\s]+\.[^@\s]+$',
    ).hasMatch(email)) {
      return 'Enter a valid email address';
    }

    return null;
  }

  static String? password(String? value) {
    final password = value ?? '';

    if (password.isEmpty) {
      return 'Password is required';
    }

    if (password.length < 8) {
      return 'Minimum 8 characters required';
    }

    return null;
  }

  static String? phone(String? value) {
    final phone = value?.trim() ?? '';

    if (phone.isEmpty) {
      return 'Phone number is required';
    }

    if (!RegExp(r'^\d{10}$').hasMatch(phone)) {
      return 'Enter a valid 10-digit phone number';
    }

    return null;
  }
}

Usage

TextFormField(
  validator: Validators.email,
)

TextFormField(
  obscureText: true,
  validator: Validators.password,
)

TextFormField(
  keyboardType: TextInputType.phone,
  validator: Validators.phone,
)

38. Complete Login Validation Example

import 'package:flutter/material.dart';

class LoginForm extends StatefulWidget {
  const LoginForm({super.key});

  @override
  State createState() => _LoginFormState();
}

class _LoginFormState extends State {
  final _formKey = GlobalKey();

  final emailController = TextEditingController();
  final passwordController = TextEditingController();

  @override
  void dispose() {
    emailController.dispose();
    passwordController.dispose();
    super.dispose();
  }

  void login() {
    if (!_formKey.currentState!.validate()) {
      return;
    }

    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(
        content: Text('Login input is valid'),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Login'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Form(
          key: _formKey,
          child: Column(
            children: [
              TextFormField(
                controller: emailController,
                keyboardType: TextInputType.emailAddress,
                decoration: const InputDecoration(
                  labelText: 'Email',
                  prefixIcon: Icon(Icons.email),
                  border: OutlineInputBorder(),
                ),
                validator: (value) {
                  final email = value?.trim() ?? '';

                  if (email.isEmpty) {
                    return 'Email is required';
                  }

                  if (!RegExp(
                    r'^[^@\s]+@[^@\s]+\.[^@\s]+$',
                  ).hasMatch(email)) {
                    return 'Enter a valid email';
                  }

                  return null;
                },
              ),
              const SizedBox(height: 16),
              TextFormField(
                controller: passwordController,
                obscureText: true,
                decoration: const InputDecoration(
                  labelText: 'Password',
                  prefixIcon: Icon(Icons.lock),
                  border: OutlineInputBorder(),
                ),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Password is required';
                  }

                  if (value.length < 8) {
                    return 'Minimum 8 characters required';
                  }

                  return null;
                },
              ),
              const SizedBox(height: 20),
              SizedBox(
                width: double.infinity,
                child: ElevatedButton(
                  onPressed: login,
                  child: const Text('Login'),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

39. Complete Registration Validation Example

import 'package:flutter/material.dart';

class RegistrationForm extends StatefulWidget {
  const RegistrationForm({super.key});

  @override
  State createState() => _RegistrationFormState();
}

class _RegistrationFormState extends State {
  final _formKey = GlobalKey();

  final nameController = TextEditingController();
  final emailController = TextEditingController();
  final phoneController = TextEditingController();
  final passwordController = TextEditingController();
  final confirmPasswordController = TextEditingController();

  String? selectedRole;
  bool acceptedTerms = false;

  @override
  void dispose() {
    nameController.dispose();
    emailController.dispose();
    phoneController.dispose();
    passwordController.dispose();
    confirmPasswordController.dispose();
    super.dispose();
  }

  void submitForm() {
    if (!_formKey.currentState!.validate()) {
      return;
    }

    if (selectedRole == null) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
          content: Text('Please select a role'),
        ),
      );
      return;
    }

    if (!acceptedTerms) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
          content: Text('Please accept the terms'),
        ),
      );
      return;
    }

    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(
        content: Text('Registration successful'),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Registration'),
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Form(
          key: _formKey,
          autovalidateMode:
              AutovalidateMode.onUserInteraction,
          child: Column(
            children: [
              TextFormField(
                controller: nameController,
                decoration: const InputDecoration(
                  labelText: 'Full Name',
                  border: OutlineInputBorder(),
                ),
                validator: (value) {
                  final name = value?.trim() ?? '';

                  if (name.isEmpty) {
                    return 'Full name is required';
                  }

                  if (name.length < 2) {
                    return 'Enter a valid name';
                  }

                  return null;
                },
              ),
              const SizedBox(height: 16),
              TextFormField(
                controller: emailController,
                keyboardType: TextInputType.emailAddress,
                decoration: const InputDecoration(
                  labelText: 'Email',
                  border: OutlineInputBorder(),
                ),
                validator: (value) {
                  final email = value?.trim() ?? '';

                  if (email.isEmpty) {
                    return 'Email is required';
                  }

                  if (!RegExp(
                    r'^[^@\s]+@[^@\s]+\.[^@\s]+$',
                  ).hasMatch(email)) {
                    return 'Enter a valid email';
                  }

                  return null;
                },
              ),
              const SizedBox(height: 16),
              TextFormField(
                controller: phoneController,
                keyboardType: TextInputType.phone,
                decoration: const InputDecoration(
                  labelText: 'Phone Number',
                  border: OutlineInputBorder(),
                ),
                validator: (value) {
                  final phone = value?.trim() ?? '';

                  if (phone.isEmpty) {
                    return 'Phone number is required';
                  }

                  if (!RegExp(r'^\d{10}$')
                      .hasMatch(phone)) {
                    return 'Enter a valid 10-digit phone number';
                  }

                  return null;
                },
              ),
              const SizedBox(height: 16),
              TextFormField(
                controller: passwordController,
                obscureText: true,
                decoration: const InputDecoration(
                  labelText: 'Password',
                  border: OutlineInputBorder(),
                ),
                validator: (value) {
                  final password = value ?? '';

                  if (password.isEmpty) {
                    return 'Password is required';
                  }

                  if (password.length < 8) {
                    return 'Minimum 8 characters required';
                  }

                  return null;
                },
              ),
              const SizedBox(height: 16),
              TextFormField(
                controller: confirmPasswordController,
                obscureText: true,
                decoration: const InputDecoration(
                  labelText: 'Confirm Password',
                  border: OutlineInputBorder(),
                ),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Confirm password is required';
                  }

                  if (value != passwordController.text) {
                    return 'Passwords do not match';
                  }

                  return null;
                },
              ),
              const SizedBox(height: 16),
              DropdownButtonFormField(
                value: selectedRole,
                decoration: const InputDecoration(
                  labelText: 'Role',
                  border: OutlineInputBorder(),
                ),
                items: const [
                  DropdownMenuItem(
                    value: 'student',
                    child: Text('Student'),
                  ),
                  DropdownMenuItem(
                    value: 'developer',
                    child: Text('Developer'),
                  ),
                  DropdownMenuItem(
                    value: 'designer',
                    child: Text('Designer'),
                  ),
                ],
                onChanged: (value) {
                  setState(() {
                    selectedRole = value;
                  });
                },
                validator: (value) {
                  if (value == null) {
                    return 'Please select a role';
                  }

                  return null;
                },
              ),
              const SizedBox(height: 8),
              CheckboxListTile(
                contentPadding: EdgeInsets.zero,
                title: const Text(
                  'I accept the Terms and Conditions',
                ),
                value: acceptedTerms,
                onChanged: (value) {
                  setState(() {
                    acceptedTerms = value ?? false;
                  });
                },
              ),
              const SizedBox(height: 16),
              SizedBox(
                width: double.infinity,
                child: ElevatedButton(
                  onPressed: submitForm,
                  child: const Text('Register'),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

40. Validation Error Messages

Good error messages should explain what the user needs to correct.

Input Example Error Message
Empty name Please enter your full name
Invalid email Enter a valid email address
Short password Password must contain at least 8 characters
Password mismatch Passwords do not match
Invalid phone Enter a valid phone number
Invalid username Use only letters, numbers, and underscores
No dropdown selection Please select a role
Empty address Address is required

41. Validation Before API Submission

Always validate form input before sending it to an API.

Future submitData() async {
  if (!_formKey.currentState!.validate()) {
    return;
  }

  final data = {
    'name': nameController.text.trim(),
    'email': emailController.text.trim(),
    'password': passwordController.text,
  };

  print(data);

  // Send data to your API here.
}

Client-side validation improves user feedback, but applications should also perform appropriate validation on the backend before storing or processing submitted data.


42. Loading State During Submission

When validation succeeds and the application starts an asynchronous operation, use a loading state to prevent repeated submissions.

bool isLoading = false;

Future submitForm() async {
  if (!_formKey.currentState!.validate()) {
    return;
  }

  setState(() {
    isLoading = true;
  });

  try {
    await Future.delayed(
      const Duration(seconds: 2),
    );

    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(
        content: Text('Data submitted successfully'),
      ),
    );
  } finally {
    if (mounted) {
      setState(() {
        isLoading = false;
      });
    }
  }
}

Submit Button

ElevatedButton(
  onPressed: isLoading ? null : submitForm,
  child: isLoading
      ? const CircularProgressIndicator()
      : const Text('Submit'),
)

43. Common Validation Mistakes

1. Creating GlobalKey Inside build()

// Avoid
Widget build(BuildContext context) {
  final formKey = GlobalKey();
  ...
}

Create the key once in the State object:

final _formKey = GlobalKey();

2. Forgetting validate()

// Incorrect
onPressed: () {
  submitData();
}

Use:

onPressed: () {
  if (_formKey.currentState!.validate()) {
    submitData();
  }
}

3. Returning an Error for Valid Input

// Incorrect
validator: (value) {
  if (value != null && value.isNotEmpty) {
    return 'Valid';
  }
  return null;
}

A valid field should return null.

4. Forgetting to Dispose Controllers

@override
void dispose() {
  emailController.dispose();
  passwordController.dispose();
  super.dispose();
}

5. Using Weak Validation Rules

Validation rules should match the actual requirements of the application instead of assuming that one generic rule is appropriate for every project.

6. Depending Only on Client-Side Validation

Client-side validation should not replace backend validation when data is sent to a server.


44. Best Practices for Email Validation

  • Use TextInputType.emailAddress.
  • Trim unnecessary whitespace.
  • Check for an acceptable email structure.
  • Provide a clear error message.
  • Do not make unnecessarily restrictive assumptions about valid email addresses.
  • Perform appropriate server-side validation when the email is submitted to a backend.

45. Best Practices for Password Validation

  • Use obscureText: true for password fields.
  • Define password rules clearly.
  • Show useful validation feedback.
  • Validate password confirmation separately.
  • Do not log passwords in production applications.
  • Do not store plaintext passwords in application databases.
  • Use secure authentication and password handling on the backend.

46. Best Practices for Other Inputs

  • Use the correct keyboard type.
  • Use input formatters where they provide useful input restrictions.
  • Validate required fields.
  • Validate numeric ranges when applicable.
  • Validate selection fields.
  • Use clear error messages.
  • Use reusable validation functions for repeated rules.
  • Test valid, invalid, empty, and boundary values.

47. Input Validation Testing Checklist

Test Expected Result
Empty required field Required-field error appears.
Valid email Email validation succeeds.
Invalid email Email error appears.
Short password Password length error appears.
Strong password Password validation succeeds according to the defined rules.
Different confirmation password Password mismatch error appears.
Matching confirmation password Confirmation validation succeeds.
Invalid phone Phone error appears.
Valid phone Phone validation succeeds.
Invalid number Number error appears.
Valid number Number validation succeeds.
No dropdown selection Selection error appears.
Valid form Submission logic executes.
Reset form Fields and validation state reset.

48. Validation Flow

User enters input
        ↓
Input is changed
        ↓
Optional automatic validation
        ↓
User taps Submit
        ↓
FormState.validate()
        ↓
Validators execute
        ↓
Input valid?
   ↙            ↘
 No              Yes
 ↓                ↓
Show errors      Process data
 ↓                ↓
Correct input    API/Database
                  submission

49. Quick Revision

  • Form groups multiple form fields.
  • TextFormField integrates text input with form validation.
  • validator contains validation rules.
  • Return an error string when input is invalid.
  • Return null when input is valid.
  • GlobalKey provides access to the form state.
  • validate() runs form validation.
  • save() executes onSaved callbacks.
  • reset() resets the form state.
  • AutovalidateMode controls automatic validation.
  • Email, password, phone, name, username, URL, number, address, and selection fields can all have application-specific validation rules.
  • Cross-field validation is useful for matching fields such as password and confirm password.
  • Reusable validator functions help keep code clean.
  • Client-side validation should be complemented by appropriate server-side validation.

50. Official Flutter Resources


51. JustAcademy Flutter Course Resources


52. Key Takeaways

  • Input validation ensures that user-entered information follows the application's rules.
  • Flutter provides built-in validation through Form, FormField, and TextFormField.
  • Email validation checks for an acceptable email structure.
  • Password validation can check length and other defined requirements.
  • Confirm password validation compares two related fields.
  • Phone, username, name, age, number, URL, address, and pincode fields can use specialized validators.
  • Dropdowns and other FormField-based controls can participate in validation.
  • AutovalidateMode can control when validation occurs automatically.
  • Reusable validation functions reduce duplicate code.
  • Controllers should be disposed when manually created.
  • Validate data before sending it to an API.
  • Server-side validation remains important for backend applications.
Previous Next
whatsapp